4.1 从代码到二进制:Go程序的编译过程
本节我们将一起探索,Go
程序在编译的时候都发生了什么、都做了哪些工作?通过本节的学习将对我们的日常编程规范、习惯起到一个正向的作用。
关于编译过程,我们将会从词法分析、语法分析、语义分析以及中间代码生成等多个方面进行讲解。
词法分析
词法分析是编译的第一阶段。在这一阶段中,编译器会将代码转换为一系列标记,在Go
语言中叫tokens
。
在这个阶段,编译器会去除空格、注释,并标记出各种代码元素,如关键字(func
、var
等)、标识符、字面量(数字、字符串等)。
我们可以通过源码cmd/compile/internal/syntax
查看到相应的实现。简单来说,就是将我们的代码进行一个整理,整理为Go
语言后续编译可识别的代码。
我们以一个例子来说明,看一下编译var a int = 10
时会发生什么。
首先分析器会逐字符的读取到我们的源码
var a int =10
,这时候读取到的是v
、a
、r
、i
、n
、t
、=
、1
、0
下一步根据关键字进行匹配,将会得到结果:
var
、a
、int
、10
在上一步中已经将这一句代码整理得到了几个关键的元素,那么下一步就是针对这几个元素作标记。如下所示:
var: 标记类型:keyword(关键字) 代表 Go 语言中的变量声明关键字 var。 a: 标记类型:identifier(标识符) 代表变量名 a。 int: 标记类型:type(类型) 代表变量的类型 int。 =: 标记类型:operator(操作符) 代表赋值操作符 =。 10: 标记类型:integer(整数常量) 代表整数常量 10。
标记完成以后,将会进一步整合形成
token
格式,如下所示:[ {Token: "var", Type: "keyword"}, {Token: "a", Type: "identifier"}, {Token: "int", Type: "type"}, {Token: "=", Type: "operator"}, {Token: "10", Type: "integer"} ]
通过上面的例子,我们大概可以知道这一步的真正作用。简单来说就是将源代码转换为编译器需要的类型。
语法、语义分析
在词法分析后,下一步就会进入到语法分析中。语法分析的主要作用是验证语法的正确性、生成语法树、报告语法错误。
我们继续上文的案例,如下所示:
语法分析接收到
tokens
结果后,根据规则检查每一个类型定义是否正确、符号是否正确检查通过后生成语法树,如下所示:
VarDecl ├── Identifier: "a" ├── Type: "int" └── Value: 10
从上面的示例我们大概可以看出,语法分析会将之前的代码进一步转换,转换为了语法数的结构,这也就是为下一步的工作继续做准备。
假如我们代码是这样的:a = b + c * d
,经过词法分析后输出如下:
[
{Token: "a", Type: "identifier"},
{Token: "=", Type: "operator"},
{Token: "b", Type: "identifier"},
{Token: "+", Type: "operator"},
{Token: "c", Type: "identifier"},
{Token: "*", Type: "operator"},
{Token: "d", Type: "identifier"}
]
那么经过语法分析后生成的语法树如下所示:
Assignment
├── Identifier: "a"
└── Expression: "+"
├── Left: "b"
└── Expression: "*"
├── Left: "c"
└── Right: "d"
这个语法树表示了表达式a = b + (c * d)
的结构,显示了乘法操作比加法操作优先级更高,并明确了赋值操作的目标是a
。
总的来说,就是语法分析器会将代码进一步转换为一个树结构,方便后续的进一步编译操作。
中间代码生成
我们知道,最终编译形成的程序都是机器码。那么中间代码其实就是指源代码与机器码之间的一个节点。
这个节点的代码不属于源代码,也不是机器码,而是介于两者之间的,独立于机器架构的,仅仅只是单纯的代码表示。
该阶段可以从源代码包:cmd/compile/internal/ir
去查看学习。
比如我们的代码如下:
func main() {
a := 10
b := 20
c := a + b
}
那么中间代码可能变成这样:
t1 = 10
t2 = 20
t3 = t1 + t2
t1
对应变量a
的赋值,t2
对应变量b
的赋值,t3
对应变量c
的赋值和a + b
的计算结果。
在比如我们上文提到的:a = b + (c * d)
,可能中间代码会变成这样:
t1 = c * d
t2 = b + t1
a = t2
优化
优化阶段,指的是编译器对之前生成的中间代码进行优化,以提高程序的执行效率。
优化的目标包括消除冗余代码、简化表达式、减少内存访问等。
优化可以分为局部优化和全局优化,分别针对局部代码块和整个程序进行优化。
这一部分的实现代码可以在源码包:cmd/compile/internal/ssa
中查看学习。
Go
语言中优化主要包括局部优化、全局优化、循环优化以及内存优化等方面。在这里我们主要说一下局部优化,方便我们理解这一阶段的工作。
常量折叠:在编译时计算常量表达式的结果,以减少运行时的计算。例如,将
3 + 5
直接替换为8
。Original: a = 3 + 5 Optimized: a = 8
常量传播:将已知的常量值传播到它们使用的地方,减少冗余计算。
Original: a = 8 b = a + 4 Optimized: b = 12
死代码消除:删除永远不会被执行的代码或计算结果不会被使用的代码。
Original: a = 3 b = a * 2 c = 4 b = a + c Optimized: a = 3 b = 7
通过上面的例子我们可以看出,这一阶段其实就是简化、优化代码。
比如说一些在代码中的计算,如果结果与运行时无关联,是固定的,那么在编译阶段就会计算完成,减少运行时的计算消耗。
代码生成、汇编与链接
这一部分也是比较重要的一个阶段,在这个阶段就会将之前的优化代码转换,形成最终的机器码。
在这一部分包含汇编与链接两个部分,可以在源码包:cmd/asm
、cmd/link
查看学习。
在这里我们简单的说一下执行步骤:
编译器将源代码编译生成中间代码,并进一步生成汇编代码。
汇编器将汇编代码转化为目标文件,包含机器指令和符号表。
链接器将多个目标文件和库文件链接在一起,解决符号引用,形成最终的可执行文件。
小结
Go
程序的编译过程包括多个阶段,从源代码的词法分析、语法分析到最终的代码生成和可执行文件的生成。每个阶段都负责不同的任务,确保源代码被正确编译为高效的机器码。
流程为:
词法分析 -> 语法分析 -> 语义分析 -> 中间代码生成 -> 优化 -> 代码生成 -> 汇编与链接
关于本节总结如下:
词法分析将源代码整合为标准代码流
语法分析检查代码流语法,形成代码树
语义分析检查代码语义是否正确、逻辑是否合理
中间代码生成对代码进行了进一步的转换
优化模块对代码检查优化,提升代码性能
优化后将中间代码转换为汇编代码或机器码
最终通过链接形成最终可执行文件